<?php

define('SECURITY_AUTHORIZE', 1);
define('SECURITY_QUESTIONS', 2);

/**
 * Штука для борьбы с подбором паролей и прочей фигни.
 *
 */
abstract class Security_Utils_BruteForce {

    /**
     * Время в минутах, на который пользователь будет забанен при попытке
     * подбора пароля.
     *
     * @var integer
     */
    const BAN_PERIOD = 10;

    /**
     * Время в минутах, в течение которого неправильные попытки считаются
     * подбором.
     *
     * @var integer
     */
    const TRIAL_PERIOD = 5;

    /**
     * Количество неудачных попыток, при которых аккаунт блокируется.
     *
     * @var integer
     */
    const TRIES_COUNT = 5;

    /**
     * Количество неудачных попыток, после которых пользователю выводится
     * предупреждение.
     *
     * @var integer
     */
    const WARNING_COUNT = 3;

    /**
     * Код события успешной авторизации .
     *
     * @var integer
     */
    const A_SUCCESS = 0;

    /**
     * Код события ошибки авторизации (попытки подбора логина/пароля).
     *
     * @var integer
     */
    const A_AUTHORIZE = SECURITY_AUTHORIZE;

    /**
     * Код события ошибки ввода ответа на секретный вопрос.
     *
     * @var integer
     */
    const A_QUESTIONS = SECURITY_QUESTIONS;

    /**
     * Регистрирует ошибку ввода данных безопасности.
     *
     * @param   integer $account_id
     * @param   integer $action
     */
    static public function register($account_id, $action) {
        database::insert('security_log', array(
            'account_id' => $account_id,
            'address' => sprintf('%u', ip2long($_SERVER['REMOTE_ADDR'])),
            'action' => (int)$action,
            'date' => new Kernel_Db_Expression('NOW()'),
        ));
    }

    /**
     * Очищает ошибки ввода данных безопасности. А оно надо? Надо. Используется
     * при успешной авторизации пользователя.
     *
     * @param   integer $account_id
     * @param   integer $action
     */
    static public function clear($account_id, $action) {
        database::delete('security_log', array(
            'account_id' => $account_id,
            'action' => (int)$action,
        ));
    }

    /**
     * Возвращает, забанен ли пользователь на указанное действие.
     *
     * @param   integer $account_id
     * @param   integer $action
     * @return  mixed
     */
    static public function banned($account_id, $action) {
        if (is_array($token = self::_tries($account_id, $action))) {
            list($count, $remain) = $token;
            if ($count >= self::TRIES_COUNT) {
                return $remain;
            }
        }
        return false;
    }

    /**
     * Возвращает, заблокирован ли аккаунт на указанное действие. В случае,
     * если да, возвращает оставшееся количество попыток.
     *
     * @param   integer $account_id
     * @param   integer $action
     * @return  mixed
     */
    static public function warning($account_id, $action) {

        return is_array($token = self::_tries($account_id, $action))
            // :KLUDGE: morozov 200220008: отнимаем единичку, т.к. данные
            // о количестве попыток загружаются и кэшируются до того,
            // как произошла последняя неудачная попытка
            && ($count = array_shift($token)) >= self::WARNING_COUNT - 1
            ? self::TRIES_COUNT - $count - 1: false;
    }

    /**
     * Возвращает, выполнил ли пользователь указанное количество неудачных попыток
     * указанного типа, и, если выполнил, время в минутах, прошедшее с момента
     * последней попытки.
     *
     * @param   integer $account_id
     * @param   integer $action
     * @return  mixed
     */
    static private function _tries($account_id, $action) {
        static $_cache = array();
        if (!isset($_cache[$account_id][$action])) {
            $_cache[$account_id][$action] = self::_fetch($account_id, $action);
        }
        return $_cache[$account_id][$action];
    }

    /**
     * Загружает из БД количество неудачных попыток указанного типа и время в
     * минутах, прошедшее с момента последней попытки.
     *
     * @param   integer $action
     * @return  mixed
     */
    static private function _fetch($account_id, $action) {

        // определяем время, оставшееся до окончания бана, если бы мы банили
        // сразу, за одну всего лишь неуспешную попытку
        $query = 'SELECT
                         CEIL(' . self::BAN_PERIOD . ' - TIME_TO_SEC(TIMEDIFF(NOW(), `date`)) / 60),
                         @last := `date`
                    FROM
                         `security_log`
                   WHERE
                         `account_id` = ' . $account_id . '
                     AND
                         `action` = ' . (int)$action . '
                     AND
                         DATE_ADD(`date`, INTERVAL ' . self::BAN_PERIOD . ' MINUTE) > NOW()
                ORDER BY
                         `date` DESC
                   LIMIT
                         1';

        // если последняя попытка находится в пределах времени бана
        if (($remain = database::fetchOne($query)) > 0) {

            // считаем количество неуспешных попыток начиная с последней за
            // установленный интервал
            $query = 'SELECT
                             COUNT(*)
                        FROM
                             `security_log`
                       WHERE
                             `account_id` = ' . $account_id . '
                         AND
                             `action` = ' . (int)$action . '
                         AND
                              DATE_ADD(`date`, INTERVAL ' . self::TRIAL_PERIOD . ' MINUTE) >= @last';

            return array(database::fetchOne($query), $remain);
        }

        return false;
    }
}
